﻿using LibNiconico.NiconicoApi.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XmlSockets;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Diagnostics;
using LibNiconico.NiconicoApi.Extentions;
using System.Threading;
using LibNiconico.NiconicoApi.Lives;
using LibNiconico.NiconicoApi.Exceptions;
using LibNiconico.NiconicoApi.Utils;
using LibNiconico.NiconicoApi.HeartBeat;
using LibNiconico.NiconicoApi.Lives.Comments.PostKey;
using LibNiconico.NiconicoApi.Lives.WayBackKey;
using LibNiconico.NiconicoApi.Lives.Comments.Broadcast;
using LibNiconico.NiconicoApi.Lives.PublishStatus;
using LibNiconico.NiconicoApi.Lives.Constant;
using LibNiconico.NiconicoApi.Internal.Http;
using LibNiconico.NiconicoApi.Lives.WatchPage;

namespace LibNiconico.NiconicoApi.Lives.Comments
{
    
    /// <summary>
    /// ニコ生コメントクライアント
    /// </summary>
    internal sealed class NicoLiveCommentClient : NicoXmlSocketCommentClient, INicoLiveCommentClient
    {
        private NicoSubmitChatInfo submitChatInfo;

        private NicoLiveHistory history;

        private Task pingLoopTask;

        

        /// <summary>
        /// コメント送信可能な状態か判定
        /// </summary>
        public bool IsSendAble
        {
            get;
            private set;
        }


        /// <summary>
        /// 接続情報
        /// </summary>
        public NicoLiveCommentConnectionInfo ConnectionInfo 
        {
            get
            {
                return base.XmlSocketConnectionInfo as NicoLiveCommentConnectionInfo;
            }
            private set
            {
                base.XmlSocketConnectionInfo = value;
            }
        }

        /// <summary>
        /// キープアライブ間隔
        /// </summary>
        public TimeSpan KeepAliveTimeSpan { get; set; }

        /// <summary>
        /// スレッドコメント受信イベントハンドラ
        /// </summary>
        public event NicoLiveThreadCommentHandler ThreadReceived;

        /// <summary>
        /// チャットコメント送信結果受信イベントハンドラ
        /// </summary>
        public event NicoLiveChatResultCommentHandler ChatResultReceived;

        /// <summary>
        /// チャットコメント受信イベントハンドラ
        /// </summary>
        public event NicoLiveChatCommentHandler ChatReceived;

        /// <summary>
        /// 切断イベントハンドラ
        /// </summary>
        public event NicoLiveCloseHandler Closed;

        private NiconicoHeartBeatClient heartBeatClient;

        /// <summary>
        /// ハートビート
        /// </summary>
        public INiconicoHeartBeatClient HeartBeatClient
        {
            get
            {
                return heartBeatClient;
            }
        }

        /// <summary>
        /// 読取例外イベントハンドラ
        /// </summary>
        public event NicoLiveReadExceptionHandler ReadException;




        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="auth"></param>
        /// <param name="connectionInfo"></param>
        /// <param name="createInfo"></param>
        /// <param name="context"></param>
        /// <param name="isSendAble"></param>
        internal NicoLiveCommentClient(NiconicoAuth auth, NicoLiveCommentConnectionInfo connectionInfo, NiconicoApiInfoGetCreateInfo createInfo, SynchronizationContext context,bool isSendAble)
            : base(auth, createInfo,context) 
        {
            ThreadReceived = null;
            ConnectionInfo = null;
            ChatReceived = null;
            ReadException = null;
            submitChatInfo = null;
            pingLoopTask = null;
            KeepAliveTimeSpan = TimeSpan.FromMinutes(1);
            history = connectionInfo.History;
            ConnectionInfo = connectionInfo;
            heartBeatClient = new NiconicoHeartBeatClient(NicoLiveConstant.NICOLIVE_URL, connectionInfo.Id, auth, context);
            heartBeatClient.Received += onReceiveHeartBeat;
            IsSendAble = isSendAble;
        }

        private void onReceiveHeartBeat(NicoHaertBeatReceivedEventArgs args)
        {
            updateTime(args.Response.Time);
        }

        /// <summary>
        /// 非同期に接続
        /// </summary>
        /// <returns></returns>
        public async Task ConnectAsync()
        {
            await this.ConnectAsync( new NicoLiveCommentConnectionExtendInfo()).ConfigureAwait(false);
            
        }

        /// <summary>
        /// 非同期に接続
        /// </summary>
        /// <param name="extendInfo"></param>
        /// <returns></returns>
        public async Task ConnectAsync( NicoLiveCommentConnectionExtendInfo extendInfo)
        {
            

            
            this.submitChatInfo = new NicoSubmitChatInfo();
            
            submitChatInfo.OpenTime = this.ConnectionInfo.OpenTime;
            submitChatInfo.EndTime = this.ConnectionInfo.EndTime;
            submitChatInfo.Thread = this.ConnectionInfo.Thread;
            submitChatInfo.Locale = this.ConnectionInfo.Locale;
            submitChatInfo.Langage = this.ConnectionInfo.Langage;
            submitChatInfo.Premium = this.ConnectionInfo.IsPremium ? 1 : 0;
            this.submitChatInfo.UserId = this.ConnectionInfo.UserId;
            

            await base.ConnectAsync(this.ConnectionInfo, extendInfo).ConfigureAwait(false);

            if (this.ConnectionInfo.IsOwner)
            {
                await SetOwnerToken().ConfigureAwait(false);
            }


            pingLoopTask = Task.Factory.StartNew(async()=>

            {
                await Pings().ConfigureAwait(false);
            });

            heartBeatClient.Start();
        }

        /// <summary>
        /// 放送主コメントを非同期に送信
        /// </summary>
        /// <param name="ownerComment"></param>
        /// <returns></returns>
        public async Task<bool> SendOwnerChatCommentAsync(NicoLiveOwnerChatComment ownerComment)
        {

            if(this.submitChatInfo.OwnerToken == "") await this.SetOwnerToken().ConfigureAwait(false);
            NicoLiveBroadcastQueryInfo query = new NicoLiveBroadcastQueryInfo();
            query.Body = ownerComment.Body;
            query.Mails = ownerComment.Mails;
            query.Is184 = ownerComment.IsAnonymous;
            query.LiveId = this.ConnectionInfo.LiveId;
            query.Token = this.submitChatInfo.OwnerToken;

            using (NiconicoInfoGetApiImpl api = new NiconicoInfoGetApiImpl(NicoLiveConstant.NICOLIVE_URL,NiconicoHttpClient.CreateNiconicoApi(this.Auth)))
            {
                
                var results = QueryStrings.Parse((await api.PostStringAsync(query).ConfigureAwait(false)).Content);

                if (results.ContainsKey("status"))
                {
                    return results["status"] == "ok";
                }
                else
                {
                    return false;
                }
            }
        }

        private async Task SetOwnerToken()
        {
            using (NiconicoInfoGetApiXmlImpl api = new NiconicoInfoGetApiXmlImpl(NicoLiveConstant.NICOLIVE_URL, NiconicoHttpClient.CreateNiconicoApi(this.Auth)))
            {
               NicoLiveGetPublishStatusResponse res = NicoLiveGetPublishStatusResponse.Parse((  await api.GetAsync(new NicoLiveGetPublishStatusQueryInfo(this.ConnectionInfo.LiveId)).ConfigureAwait(false)).Content);
               this.submitChatInfo.OwnerToken = res.PublishStatus.Token;
            }
        }

        private async Task<string> getPostKeyAsync()
        {
            this.submitChatInfo.LastBlockNo = this.submitChatInfo.LastCommentNo / 100;
            using (NiconicoInfoGetApiQueryStringsImpl api = new NiconicoInfoGetApiQueryStringsImpl(NicoLiveConstant.NICOLIVE_URL, NiconicoHttpClient.CreateNiconicoApi(this.Auth)))
            {
                var queryInfo = new NicoLiveGetPostKeyQueryInfo(this.submitChatInfo.Thread, this.submitChatInfo.LastBlockNo, this.history, this.submitChatInfo.UserId);
                var res = NicoLiveGetPostkeyResponse.Parse((await api.GetAsync(queryInfo).ConfigureAwait(false)).Content);
                return res.PostKey;



            }
        }

        private async Task<string> getWayBackKeyAsync()
        {
            using (var api = new NiconicoInfoGetApiQueryStringsImpl(NicoLiveConstant.NICOLIVE_URL, NiconicoHttpClient.CreateNiconicoApi(this.Auth)))
            {
                return NicoLiveGetWayBackKeyResponse.Parse((await api.GetAsync(new NicoLiveGetWayBackKeyQueryInfo(this.submitChatInfo.Thread)).ConfigureAwait(false)).Content).WayBackKey;
            }
        }

        /// <summary>
        /// 過去コメント受信要求を非同期に送信
        /// </summary>
        /// <param name="queryInfo"></param>
        /// <returns></returns>
        public async Task SendWayBackRequest(NicoLiveGetWayBackRequestQueryInfo queryInfo)
        {
            if (submitChatInfo.MinimumReceiveDateTime == DateTimeExtention.Epoch) return;
            XElement message = new XElement("thread");
            message.SetAttributeValue("thread", this.submitChatInfo.Thread);
            message.SetAttributeValue("res_from", queryInfo.ResFrom);
            message.SetAttributeValue("waybackkey", await getWayBackKeyAsync().ConfigureAwait(false));
            message.SetAttributeValue("user_id", this.submitChatInfo.UserId);
            message.SetAttributeValue("version", queryInfo.Version);
            message.SetAttributeValue("scores", queryInfo.Scores);
            message.SetAttributeValue("when", DateTimeExtention.ToUnixTime( submitChatInfo.MinimumReceiveDateTime));

            XElement ping = new XElement("ping");
            ping.Value = "EOT";

            await xmlSocket.SendAsync(writer =>
            {
                message.WriteTo(writer);
                writer.Flush();
                ping.WriteTo(writer);
                writer.Flush();
            }).ConfigureAwait(false);

        }

        private async Task sendPingAsync()
        {

            if (this.Connected)
            {
                XElement ping = new XElement("ping");
                ping.Value = "PING";
                await xmlSocket.SendAsync(ping).ConfigureAwait(false);
            }
            
        }



        private async Task Pings()
        {

            int exceptionCount = 0;
           
                
            do{
                try
                {
                    if (this.ConnectionInfo.IsOwner)
                    {
                        await this.sendPingAsync().ConfigureAwait(false);

                        DateTime now = DateTime.Now;
                        if ((now - this.submitChatInfo.LastReceiveLocalTime).TotalMinutes > 2) await this.sendKeepAlive().ConfigureAwait(false);

                    }
                }
                catch (Exception e)
                {
                    if (exceptionCount > 3)
                    {
                        this.onClose(new NicoLiveCommentClientCloseEventArgs(this,e,e.Message));
                        this.Close();
                        
                    }

                    exceptionCount++;
               
                }

#if !NET40
                await Task.Delay(KeepAliveTimeSpan).ConfigureAwait(false);
#else
                await TaskEx.Delay(KeepAliveTimeSpan).ConfigureAwait(false);
#endif

            } while (this.Connected);
            

                
            
            

        }

        private async Task sendKeepAlive()
        {
            if (this.Connected)
            {
                await this.SendOwnerChatCommentAsync(new NicoLiveOwnerChatComment { Body = "/keepalive" }).ConfigureAwait(false);
            }
            
        }


        /// <summary>
        /// 非同期にチャットコメントを送信
        /// </summary>
        /// <param name="submitChatComment"></param>
        /// <returns></returns>
        public async Task SendAsync(NicoLiveSubmitChatComment submitChatComment)
        {
            if (!this.IsSendAble) throw new NicoApiException("このクライアントではコメントの送信を行うことができません");


            this.submitChatInfo.LastPostKey = await getPostKeyAsync().ConfigureAwait(false);


            await base.SendAsync(submitChatComment, message =>
            {
                message.SetAttributeValue("vpos", calcVpos());
                message.SetAttributeValue("ticket", this.submitChatInfo.Ticket);
                message.SetAttributeValue("postkey", this.submitChatInfo.LastPostKey);
                message.SetAttributeValue("locale", this.submitChatInfo.Langage);
                message.SetAttributeValue("user_id", this.submitChatInfo.UserId);
                message.SetAttributeValue("name", submitChatComment.Name);
                message.SetAttributeValue("premium", this.submitChatInfo.Premium);

            }).ConfigureAwait(false);

        }

        private int calcVpos()
        {
            var elapsed = (submitChatInfo.LastReceiveLocalTime.ToUniversalTime() + ( DateTime.UtcNow - submitChatInfo.LastReceiveLocalTime.ToUniversalTime())).ToLocalTime();
           
            return (int)(elapsed - submitChatInfo.OpenTime).TotalMilliseconds / 10;
                
            
        }
        private void updateTime(DateTime dateTime)
        {
            submitChatInfo.LastReceiveLocalTime = dateTime;
        }
        

        /// <summary>
        /// スレッドコメント受信イベントを発生させる
        /// </summary>
        /// <param name="message"></param>
        protected override void OnProtectedReadThread(XElement message)
        {
            
            NicoLiveThreadComment threadComment = NicoLiveThreadComment.Parse(message);
            submitChatInfo.Ticket = threadComment.Ticket;

            updateTime(threadComment.ServerTime);

            generateEvent(() =>
            {
                if (ThreadReceived != null) ThreadReceived(new NicoLiveCommentClientThreadCommentEventArgs(this, threadComment));
            });
             
        }

        



        /// <summary>
        /// チャットコメント受信イベントを発生させる
        /// </summary>
        /// <param name="message"></param>
        protected override void OnProtectedReadChatResult(XElement message)
        {
            NicoLiveChatResultComment chatResult = NicoLiveChatResultComment.Parse(message);
            generateEvent(() =>
            {
                if (ChatResultReceived != null) ChatResultReceived(
                     new NicoLiveCommentClientChatResultCommentEventArgs(this, chatResult)
                     );
            });
        }

        /// <summary>
        /// チャットコメント受信イベントを発生させる
        /// </summary>
        /// <param name="message"></param>
        private  void onReadChat(XElement message)
        {
            NicoLiveChatComment chatComment = NicoLiveChatComment.Parse(message);
            this.submitChatInfo.LastCommentNo = chatComment.No;
            updateTime(chatComment.Date);
            if (this.submitChatInfo.MinimumReceiveDateTime == DateTimeExtention.Epoch || this.submitChatInfo.MinimumReceiveDateTime > chatComment.Date)
            {
                this.submitChatInfo.MinimumReceiveDateTime = chatComment.Date;
            }



            
        }

        private void checkDisconnect(NicoLiveChatComment chatComment)
        {
            if (chatComment.Body == "/disconnect" && (chatComment.Premium.UserType == NicoChatCommentUserType.Manager || chatComment.Premium.UserType == NicoChatCommentUserType.Alert))
            {

                this.onClose(new NicoLiveCommentClientCloseEventArgs(this, null, "/disconnectコメントを受信したため切断します"));
                this.Close();
                
            }
        }

        /// <summary>
        /// 切断イベントを発生させる
        /// </summary>
        /// <param name="reason"></param>
        /// <param name="e"></param>
        protected override void OnProtectedClose(string reason, Exception e)
        {
            generateEvent(() =>
            {
                onClose(new NicoLiveCommentClientCloseEventArgs(this, e, reason));
            });
        }



        /// <summary>
        /// 読取例外イベントを発生させる
        /// </summary>
        /// <param name="e"></param>
        protected override void OnProtectedReadException(Exception e)
        {
            generateEvent(() =>
            {
                if (ReadException != null) { ReadException(new NicoLiveReadExceptionEventArgs(this, e)); }
                else { throw e; }
            });

        }

        private void onClose(NicoLiveCommentClientCloseEventArgs args)
        {
            
            if (Closed != null) Closed(args);
            else if (args.Exception != null) throw args.Exception;
            
        }

        /// <summary>
        /// このオブジェクトが保有しているリソースを解放する
        /// </summary>
        public override void Dispose()
        {
            

            this.heartBeatClient.Dispose();

            

            base.Dispose();

            pingLoopTask = null;

            ThreadReceived = null;

            ChatResultReceived = null;

            ChatReceived = null;

            Closed = null;

            ReadException = null;
        }


        protected override void OnReadChat(IEnumerable<XElement> messages)
        {
            var chatComments = new List<NicoLiveChatComment>();
            foreach (var message in messages)
            {
                ReadXmlMessage(message, (chat) =>
                {
                    onReadChat(chat);
                    chatComments.Add( NicoLiveChatComment.Parse(chat));
                });



            }


            generateEvent(() =>
            {
                if (ChatReceived != null && chatComments.Count > 0 ) ChatReceived(new NicoLiveCommentClientChatCommentEventArgs(this, chatComments));

           

                foreach (var chatComment in chatComments)
                {
                    checkDisconnect(chatComment);
                }

            });

            
        }


        private void generateEvent(Action action)
        {
            if (SynchronizationContext != null)
            {
                SynchronizationContext.Post((s) =>
                {
                    action();
                }, null);
            }
            else
            {
                action();
            }
        }
    }
     
}
